Initial

Brightcells 7 jaren geleden
commit
f9ee4d9164
78 gewijzigde bestanden met toevoegingen van 2365 en 0 verwijderingen
  1. 29 0
      .editorconfig
  2. 66 0
      .gitignore
  3. 8 0
      .isort.cfg
  4. 0 0
      account/__init__.py
  5. 15 0
      account/admin.py
  6. 43 0
      account/migrations/0001_initial.py
  7. 0 0
      account/migrations/__init__.py
  8. 62 0
      account/models.py
  9. 4 0
      account/tests.py
  10. 4 0
      account/views.py
  11. 0 0
      api/__init__.py
  12. 4 0
      api/admin.py
  13. 0 0
      api/migrations/__init__.py
  14. 4 0
      api/models.py
  15. 35 0
      api/oauth_views.py
  16. 4 0
      api/tests.py
  17. 16 0
      api/urls.py
  18. 4 0
      api/views.py
  19. 9 0
      check.sh
  20. 0 0
      codes/__init__.py
  21. 14 0
      codes/admin.py
  22. 8 0
      codes/apps.py
  23. 32 0
      codes/migrations/0001_initial.py
  24. 0 0
      codes/migrations/__init__.py
  25. 20 0
      codes/models.py
  26. 7 0
      codes/tests.py
  27. 7 0
      codes/views.py
  28. 0 0
      course/__init__.py
  29. 26 0
      course/basemodels.py
  30. 36 0
      course/decorators.py
  31. 27 0
      course/deploy.bak/course.ini
  32. 40 0
      course/deploy.bak/course_nginx.conf
  33. 10 0
      course/deploy.bak/course_supervisor.ini
  34. 15 0
      course/deploy.bak/uwsgi_params
  35. 16 0
      course/func_settings.py
  36. 43 0
      course/local_settings_bak.py
  37. 33 0
      course/oauth_settings.py
  38. 266 0
      course/settings.py
  39. 331 0
      course/static/course/js/jswe-0.0.1.js
  40. 36 0
      course/urls.py
  41. 17 0
      course/wsgi.py
  42. 0 0
      courses/__init__.py
  43. 20 0
      courses/admin.py
  44. 8 0
      courses/apps.py
  45. 57 0
      courses/migrations/0001_initial.py
  46. 21 0
      courses/migrations/0002_coursevideoinfo_course_video.py
  47. 0 0
      courses/migrations/__init__.py
  48. 87 0
      courses/models.py
  49. 7 0
      courses/tests.py
  50. 7 0
      courses/views.py
  51. 3 0
      isort.sh
  52. 23 0
      manage.py
  53. 0 0
      page/__init__.py
  54. 4 0
      page/admin.py
  55. 67 0
      page/code_views.py
  56. 47 0
      page/info_views.py
  57. 37 0
      page/list_views.py
  58. 31 0
      page/migrations/0001_initial.py
  59. 0 0
      page/migrations/__init__.py
  60. 33 0
      page/models.py
  61. 67 0
      page/static/page/css/weui.ext.css
  62. BIN
      page/static/page/img/code_cover.png
  63. 135 0
      page/templates/page/course_code.html
  64. 124 0
      page/templates/page/course_info.html
  65. 77 0
      page/templates/page/course_list.html
  66. 4 0
      page/tests.py
  67. 12 0
      page/urls.py
  68. 4 0
      page/views.py
  69. 9 0
      pep8.sh
  70. 40 0
      requirements.txt
  71. 0 0
      utils/__init__.py
  72. 0 0
      utils/error/__init__.py
  73. 51 0
      utils/error/errno_utils.py
  74. 18 0
      utils/error/response_utils.py
  75. 0 0
      utils/redis/__init__.py
  76. 6 0
      utils/redis/connect.py
  77. 68 0
      utils/redis/rkeys.py
  78. 7 0
      utils/url_utils.py

+ 29 - 0
.editorconfig

@@ -0,0 +1,29 @@
1
+# EditorConfig is awesome: http://EditorConfig.org
2
+
3
+# top-most EditorConfig file
4
+root = true
5
+
6
+# Unix-style newlines with a newline ending every file
7
+[*]
8
+end_of_line = lf
9
+insert_final_newline = false
10
+
11
+# 4 space indentation
12
+[*.py]
13
+indent_style = space
14
+indent_size = 4
15
+
16
+# Tab indentation (no size specified)
17
+[*.js]
18
+indent_style = space
19
+indent_size = 4
20
+
21
+# Tab indentation (no size specified)
22
+[*.html]
23
+indent_style = space
24
+indent_size = 4
25
+
26
+# Matches the exact files either package.json or .travis.yml
27
+[{package.json,.travis.yml}]
28
+indent_style = space
29
+indent_size = 2

+ 66 - 0
.gitignore

@@ -0,0 +1,66 @@
1
+# Byte-compiled / optimized / DLL files
2
+__pycache__/
3
+*.py[cod]
4
+*.swp
5
+# C extensions
6
+*.so
7
+
8
+# Distribution / packaging
9
+bin/
10
+build/
11
+develop-eggs/
12
+dist/
13
+eggs/
14
+lib/
15
+lib64/
16
+parts/
17
+sdist/
18
+venv/
19
+var/
20
+static/upload/
21
+*.egg-info/
22
+.installed.cfg
23
+*.egg
24
+*.sublime*
25
+
26
+# Installer logs
27
+pip-log.txt
28
+pip-delete-this-directory.txt
29
+
30
+# Unit test / coverage reports
31
+.tox/
32
+.coverage
33
+.cache
34
+nosetests.xml
35
+coverage.xml
36
+
37
+# Translations
38
+# *.mo
39
+
40
+# Mr Developer
41
+.mr.developer.cfg
42
+.project
43
+.pydevproject
44
+.settings
45
+# Rope
46
+.ropeproject
47
+
48
+# Django stuff:
49
+*.log
50
+*.pot
51
+
52
+# Sphinx documentation
53
+docs/_build/
54
+
55
+
56
+# Ignore For zhTimer
57
+.DS_Store
58
+db.sqlite3
59
+local_settings.py
60
+
61
+.idea/
62
+media/
63
+collect_static/
64
+
65
+# Special File
66
+*download.html

+ 8 - 0
.isort.cfg

@@ -0,0 +1,8 @@
1
+# See the menu of settings available here:
2
+#   https://github.com/timothycrosley/isort/wiki/isort-Settings
3
+
4
+[settings]
5
+indent='    '
6
+line_length=120
7
+lines_after_imports=2
8
+skip=migrations

+ 0 - 0
account/__init__.py


+ 15 - 0
account/admin.py

@@ -0,0 +1,15 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from djadmin import ReadonlyModelAdmin
4
+from django.contrib import admin
5
+
6
+from account.models import UserInfo
7
+
8
+
9
+class UserInfoAdmin(ReadonlyModelAdmin, admin.ModelAdmin):
10
+    list_display = ('user_id', 'unionid', 'openid', 'name', 'sex', 'nickname', 'phone', 'country', 'province', 'city', 'location', 'status', 'created_at', 'updated_at')
11
+    list_filter = ('sex', 'status')
12
+    actions = None
13
+
14
+
15
+admin.site.register(UserInfo, UserInfoAdmin)

+ 43 - 0
account/migrations/0001_initial.py

@@ -0,0 +1,43 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.11.3 on 2017-09-24 14:50
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+import shortuuidfield.fields
7
+
8
+
9
+class Migration(migrations.Migration):
10
+
11
+    initial = True
12
+
13
+    dependencies = [
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='UserInfo',
19
+            fields=[
20
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
22
+                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
23
+                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
24
+                ('user_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
25
+                ('unionid', models.CharField(blank=True, db_index=True, help_text='\u5fae\u4fe1 Unionid', max_length=255, null=True, unique=True, verbose_name='unionid')),
26
+                ('openid', models.CharField(blank=True, db_index=True, help_text='\u5fae\u4fe1 Openid', max_length=255, null=True, unique=True, verbose_name='openid')),
27
+                ('name', models.CharField(blank=True, help_text='\u7528\u6237\u59d3\u540d', max_length=255, null=True, verbose_name='name')),
28
+                ('sex', models.IntegerField(choices=[(1, '\u7537'), (0, '\u5973')], default=1, help_text='\u7528\u6237\u6027\u522b', verbose_name='sex')),
29
+                ('nickname', models.CharField(blank=True, help_text='\u7528\u6237\u6635\u79f0', max_length=255, null=True, verbose_name='nickname')),
30
+                ('avatar', models.CharField(blank=True, help_text='\u7528\u6237\u5934\u50cf', max_length=255, null=True, verbose_name='avatar')),
31
+                ('phone', models.CharField(blank=True, db_index=True, help_text='\u7528\u6237\u7535\u8bdd', max_length=255, null=True, unique=True, verbose_name='phone')),
32
+                ('country', models.CharField(blank=True, help_text='\u7528\u6237\u56fd\u5bb6', max_length=255, null=True, verbose_name='country')),
33
+                ('province', models.CharField(blank=True, help_text='\u7528\u6237\u7701\u4efd', max_length=255, null=True, verbose_name='province')),
34
+                ('city', models.CharField(blank=True, help_text='\u7528\u6237\u57ce\u5e02', max_length=255, null=True, verbose_name='city')),
35
+                ('location', models.CharField(blank=True, help_text='\u7528\u6237\u5730\u5740', max_length=255, null=True, verbose_name='location')),
36
+                ('user_status', models.IntegerField(choices=[(0, '\u672a\u9a8c\u8bc1'), (1, '\u5df2\u6fc0\u6d3b'), (2, '\u5df2\u7981\u7528'), (3, '\u5df2\u5220\u9664')], default=0, verbose_name='user_status')),
37
+            ],
38
+            options={
39
+                'verbose_name': 'userinfo',
40
+                'verbose_name_plural': 'userinfo',
41
+            },
42
+        ),
43
+    ]

+ 0 - 0
account/migrations/__init__.py


+ 62 - 0
account/models.py

@@ -0,0 +1,62 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.db import models
4
+from django.utils.translation import ugettext_lazy as _
5
+from shortuuidfield import ShortUUIDField
6
+
7
+from course.basemodels import CreateUpdateMixin
8
+
9
+
10
+class UserInfo(CreateUpdateMixin):
11
+    UNVERIFIED = 0
12
+    ACTIVATED = 1
13
+    DISABLED = 2
14
+    DELETED = 3
15
+
16
+    USER_STATUS = (
17
+        (UNVERIFIED, u'未验证'),
18
+        (ACTIVATED, u'已激活'),
19
+        (DISABLED, u'已禁用'),
20
+        (DELETED, u'已删除'),
21
+    )
22
+
23
+    MALE = 1
24
+    FEMALE = 0
25
+
26
+    SEX_TYPE = (
27
+        (MALE, u'男'),
28
+        (FEMALE, u'女'),
29
+    )
30
+
31
+    user_id = ShortUUIDField(_(u'user_id'), max_length=255, help_text=u'用户唯一标识', db_index=True, unique=True)
32
+
33
+    # 微信授权用户
34
+    unionid = models.CharField(_(u'unionid'), max_length=255, blank=True, null=True, help_text=u'微信 Unionid', db_index=True, unique=True)
35
+    openid = models.CharField(_(u'openid'), max_length=255, blank=True, null=True, help_text=u'微信 Openid', db_index=True, unique=True)
36
+    # 用户基本信息
37
+    name = models.CharField(_(u'name'), max_length=255, blank=True, null=True, help_text=u'用户姓名')
38
+    sex = models.IntegerField(_(u'sex'), choices=SEX_TYPE, default=MALE, help_text=u'用户性别')
39
+    nickname = models.CharField(_(u'nickname'), max_length=255, blank=True, null=True, help_text=u'用户昵称')
40
+    avatar = models.CharField(_(u'avatar'), max_length=255, blank=True, null=True, help_text=u'用户头像')
41
+    phone = models.CharField(_(u'phone'), max_length=255, blank=True, null=True, help_text=u'用户电话', db_index=True, unique=True)
42
+    country = models.CharField(_(u'country'), max_length=255, blank=True, null=True, help_text=u'用户国家')
43
+    province = models.CharField(_(u'province'), max_length=255, blank=True, null=True, help_text=u'用户省份')
44
+    city = models.CharField(_(u'city'), max_length=255, blank=True, null=True, help_text=u'用户城市')
45
+    location = models.CharField(_(u'location'), max_length=255, blank=True, null=True, help_text=u'用户地址')
46
+
47
+    user_status = models.IntegerField(_(u'user_status'), choices=USER_STATUS, default=UNVERIFIED)
48
+
49
+    class Meta:
50
+        verbose_name = _(u'userinfo')
51
+        verbose_name_plural = _(u'userinfo')
52
+
53
+    def __unicode__(self):
54
+        return unicode(self.pk)
55
+
56
+    @property
57
+    def data(self):
58
+        return {
59
+            'user_id': self.user_id,
60
+            'nickname': self.nickname,
61
+            'avatar': self.avatar,
62
+        }

+ 4 - 0
account/tests.py

@@ -0,0 +1,4 @@
1
+from django.test import TestCase
2
+
3
+
4
+# Create your tests here.

+ 4 - 0
account/views.py

@@ -0,0 +1,4 @@
1
+from django.shortcuts import render
2
+
3
+
4
+# Create your views here.

+ 0 - 0
api/__init__.py


+ 4 - 0
api/admin.py

@@ -0,0 +1,4 @@
1
+from django.contrib import admin
2
+
3
+
4
+# Register your models here.

+ 0 - 0
api/migrations/__init__.py


+ 4 - 0
api/models.py

@@ -0,0 +1,4 @@
1
+from django.db import models
2
+
3
+
4
+# Create your models here.

+ 35 - 0
api/oauth_views.py

@@ -0,0 +1,35 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from __future__ import division
4
+
5
+from django.conf import settings
6
+from django.core.urlresolvers import reverse
7
+from django.db import transaction
8
+from django.shortcuts import redirect
9
+from furl import furl
10
+from logit import logit
11
+
12
+from account.models import UserInfo
13
+from utils.redis.connect import r
14
+
15
+
16
+@logit
17
+@transaction.atomic
18
+def oauth_redirect(request):
19
+    unique_identifier = request.GET.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
20
+
21
+    user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
22
+    user.unionid = request.GET.get('unionid', '')
23
+    user.openid = request.GET.get('openid', '')
24
+    user.nickname = request.GET.get('nickname', '')
25
+    user.avatar = request.GET.get('headimgurl', '')
26
+    user.save()
27
+
28
+    token_check_key = user.user_id
29
+
30
+    query_params = {
31
+        settings.TOKEN_CHECK_KEY: token_check_key,
32
+        'vtoken': r.token(token_check_key, ex=False, buf=False),
33
+    }
34
+
35
+    return redirect(furl(reverse('page:user_oauth')).add(request.GET).add(query_params).url)

+ 4 - 0
api/tests.py

@@ -0,0 +1,4 @@
1
+from django.test import TestCase
2
+
3
+
4
+# Create your tests here.

+ 16 - 0
api/urls.py

@@ -0,0 +1,16 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf.urls import url
4
+
5
+from api import oauth_views
6
+from page import code_views, info_views, list_views
7
+
8
+
9
+urlpatterns = [
10
+    url(r'^code/exchange$', code_views.code_exchange, name='code_exchange'),
11
+]
12
+
13
+urlpatterns += [
14
+    url(r'^3rd/or$', oauth_views.oauth_redirect, name='3rd_or'),
15
+    url(r'^3rd/oauth_redirect$', oauth_views.oauth_redirect, name='3rd_oauth_redirect'),
16
+]

+ 4 - 0
api/views.py

@@ -0,0 +1,4 @@
1
+from django.shortcuts import render
2
+
3
+
4
+# Create your views here.

+ 9 - 0
check.sh

@@ -0,0 +1,9 @@
1
+#!/bin/bash
2
+
3
+echo '>> iSort'
4
+./isort.sh
5
+echo
6
+
7
+echo '>> PEP8'
8
+./pep8.sh
9
+echo

+ 0 - 0
codes/__init__.py


+ 14 - 0
codes/admin.py

@@ -0,0 +1,14 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from djadmin import ExportExcelModelAdmin, ReadonlyModelAdmin
4
+from django.contrib import admin
5
+
6
+from codes.models import CourseCodeInfo
7
+
8
+
9
+class CourseCodeInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin):
10
+    list_display = ('code', 'exchanged', 'user_id', 'status', 'created_at', 'updated_at')
11
+    list_filter = ('exchanged', 'status')
12
+
13
+
14
+admin.site.register(CourseCodeInfo, CourseCodeInfoAdmin)

+ 8 - 0
codes/apps.py

@@ -0,0 +1,8 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.apps import AppConfig
5
+
6
+
7
+class CodesConfig(AppConfig):
8
+    name = 'codes'

+ 32 - 0
codes/migrations/0001_initial.py

@@ -0,0 +1,32 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.11.3 on 2017-09-24 14:50
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+
7
+
8
+class Migration(migrations.Migration):
9
+
10
+    initial = True
11
+
12
+    dependencies = [
13
+    ]
14
+
15
+    operations = [
16
+        migrations.CreateModel(
17
+            name='CourseCodeInfo',
18
+            fields=[
19
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
+                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
21
+                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
22
+                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
23
+                ('code', models.CharField(blank=True, db_index=True, help_text='\u5151\u6362\u7801', max_length=255, null=True, verbose_name='code')),
24
+                ('exchanged', models.BooleanField(db_index=True, default=False, help_text='\u5151\u6362\u72b6\u6001', verbose_name='exchanged')),
25
+                ('user_id', models.CharField(blank=True, db_index=True, help_text='\u7528\u6237\u552f\u4e00\u6807\u8bc6', max_length=255, null=True, unique=True, verbose_name='user_id')),
26
+            ],
27
+            options={
28
+                'verbose_name': 'coursecodeinfo',
29
+                'verbose_name_plural': 'coursecodeinfo',
30
+            },
31
+        ),
32
+    ]

+ 0 - 0
codes/migrations/__init__.py


+ 20 - 0
codes/models.py

@@ -0,0 +1,20 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.db import models
4
+from django.utils.translation import ugettext_lazy as _
5
+
6
+from course.basemodels import CreateUpdateMixin
7
+
8
+
9
+class CourseCodeInfo(CreateUpdateMixin):
10
+    code = models.CharField(_(u'code'), max_length=255, blank=True, null=True, help_text=u'兑换码', db_index=True)
11
+    exchanged = models.BooleanField(_(u'exchanged'), default=False, help_text=_(u'兑换状态'), db_index=True)
12
+
13
+    user_id = models.CharField(_(u'user_id'), max_length=255, blank=True, null=True, help_text=u'用户唯一标识', db_index=True, unique=True)
14
+
15
+    class Meta:
16
+        verbose_name = _(u'coursecodeinfo')
17
+        verbose_name_plural = _(u'coursecodeinfo')
18
+
19
+    def __unicode__(self):
20
+        return unicode(self.pk)

+ 7 - 0
codes/tests.py

@@ -0,0 +1,7 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.test import TestCase
5
+
6
+
7
+# Create your tests here.

+ 7 - 0
codes/views.py

@@ -0,0 +1,7 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.shortcuts import render
5
+
6
+
7
+# Create your views here.

+ 0 - 0
course/__init__.py


+ 26 - 0
course/basemodels.py

@@ -0,0 +1,26 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.db import models
4
+from django.utils.translation import ugettext_lazy as _
5
+
6
+
7
+class CreateUpdateMixin(models.Model):
8
+    status = models.BooleanField(_(u'status'), default=True, help_text=_(u'状态'), db_index=True)
9
+    created_at = models.DateTimeField(_(u'created_at'), auto_now_add=True, editable=True, help_text=_(u'创建时间'))
10
+    updated_at = models.DateTimeField(_(u'updated_at'), auto_now=True, editable=True, help_text=_(u'更新时间'))
11
+
12
+    class Meta:
13
+        abstract = True
14
+
15
+
16
+class SexChoicesMixin(models.Model):
17
+    MALE = 1
18
+    FEMALE = 0
19
+
20
+    SEX_TYPE = (
21
+        (MALE, u'男'),
22
+        (FEMALE, u'女'),
23
+    )
24
+
25
+    class Meta:
26
+        abstract = True

+ 36 - 0
course/decorators.py

@@ -0,0 +1,36 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from functools import wraps
4
+
5
+from django.conf import settings
6
+from django.shortcuts import redirect, render
7
+from furl import furl
8
+from pywe_oauth import get_oauth_redirect_url
9
+
10
+from utils.redis.connect import r
11
+
12
+
13
+def check_token(func=None):
14
+    def decorator(func):
15
+        @wraps(func)
16
+        def returned_wrapper(request, *args, **kwargs):
17
+            vtoken = request.GET.get('vtoken', '') or request.POST.get('vtoken', '')
18
+            if not settings.DEBUG:
19
+                if not request.wechat:
20
+                    return render(request, 'django_we/errmsg.html', {'title': '错误', 'errmsg': '请在微信中打开'})
21
+                token_check_key = request.GET.get(settings.TOKEN_CHECK_KEY, '') or request.POST.get(settings.TOKEN_CHECK_KEY, '')
22
+                if not r.token_exists(token_check_key, vtoken):
23
+                    # 3rd OAuth
24
+                    return redirect(settings.WECHAT_OAUTH2_REDIRECT_URL)
25
+                    # Current OAuth
26
+                    redirect_url = furl(settings.WECHAT_OAUTH2_REDIRECT_ENTRY).add({}).url
27
+                    return redirect(get_oauth_redirect_url(settings.WECHAT_OAUTH2_REDIRECT_URI, 'snsapi_userinfo', redirect_url))
28
+            return func(request, *args, **kwargs)
29
+        return returned_wrapper
30
+
31
+    if not func:
32
+        def foo(func):
33
+            return decorator(func)
34
+        return foo
35
+
36
+    return decorator(func)

+ 27 - 0
course/deploy.bak/course.ini

@@ -0,0 +1,27 @@
1
+# course_uwsgi.ini file
2
+[uwsgi]
3
+
4
+# Django-related settings
5
+# the base directory (full path)
6
+chdir           = /home/paiai/work/course
7
+# Django's wsgi file
8
+module          = course.wsgi
9
+# the virtualenv (full path)
10
+# home            = /path/to/virtualenv
11
+
12
+# process-related settings
13
+# master
14
+master          = true
15
+# maximum number of worker processes
16
+processes       = 10
17
+# the socket (use the full path to be safe
18
+socket          = /home/paiai/work/course/course/deploy/course.sock
19
+# ... with appropriate permissions - may be needed
20
+chmod-socket    = 777
21
+# clear environment on exit
22
+vacuum          = true
23
+
24
+# 11: Resource temporarily unavailable
25
+reload-mercy    = 64
26
+max-requests    = 8192
27
+listen          = 4096

+ 40 - 0
course/deploy.bak/course_nginx.conf

@@ -0,0 +1,40 @@
1
+# course_nginx.conf
2
+
3
+# the upstream component nginx needs to connect to
4
+upstream course {
5
+    # server unix:///home/paiai/work/course/course/deploy/course.sock; # for a file socket
6
+    server 127.0.0.1:8888; # for a web port socket (we'll use this first)
7
+}
8
+
9
+# configuration of the server
10
+server {
11
+    # the port your site will be served on
12
+    listen      80;
13
+    # the domain name it will serve for
14
+    server_name .a.com; # substitute your machine's IP address or FQDN
15
+    charset     utf-8;
16
+
17
+    # max upload size
18
+    client_max_body_size 75M;   # adjust to taste
19
+
20
+    # JS接口安全域名 & 业务域名 验证
21
+    location /xxx.txt {
22
+        alias /home/paiai/work/course/docs/we/xxx.txt;
23
+    }
24
+
25
+    # Django media
26
+    location /media  {
27
+        alias /home/paiai/work/course/media;  # your Django project's media files - amend as required
28
+    }
29
+
30
+    location /static {
31
+        alias /home/paiai/work/course/collect_static; # your Django project's static files - amend as required
32
+    }
33
+
34
+    # Finally, send all non-media requests to the Django server.
35
+    location / {
36
+        # uwsgi_pass  course;
37
+        proxy_pass  http://course;
38
+        include     /home/paiai/work/course/course/deploy/uwsgi_params; # the uwsgi_params file you installed
39
+    }
40
+}

+ 10 - 0
course/deploy.bak/course_supervisor.ini

@@ -0,0 +1,10 @@
1
+[program:course]
2
+command=/home/paiai/env/bin/uwsgi --ini /home/paiai/work/course/course/deploy/course.ini
3
+autostart=true
4
+autorestart=true
5
+startretries=3
6
+exitcodes=0,1,2
7
+stopsignal=QUIT
8
+stdout_logfile=/var/log/supervisor_course_access.log
9
+stderr_logfile=/var/log/supervisor_course_error.log
10
+user=diors

+ 15 - 0
course/deploy.bak/uwsgi_params

@@ -0,0 +1,15 @@
1
+uwsgi_param	QUERY_STRING		$query_string;
2
+uwsgi_param	REQUEST_METHOD		$request_method;
3
+uwsgi_param	CONTENT_TYPE		$content_type;
4
+uwsgi_param	CONTENT_LENGTH		$content_length;
5
+
6
+uwsgi_param	REQUEST_URI		$request_uri;
7
+uwsgi_param	PATH_INFO		$document_uri;
8
+uwsgi_param	DOCUMENT_ROOT		$document_root;
9
+uwsgi_param	SERVER_PROTOCOL		$server_protocol;
10
+uwsgi_param	UWSGI_SCHEME		$scheme;
11
+
12
+uwsgi_param	REMOTE_ADDR		$remote_addr;
13
+uwsgi_param	REMOTE_PORT		$remote_port;
14
+uwsgi_param	SERVER_PORT		$server_port;
15
+uwsgi_param	SERVER_NAME		$server_name;

+ 16 - 0
course/func_settings.py

@@ -0,0 +1,16 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import redis_extensions as redis
4
+
5
+
6
+def redis_conf(conf):
7
+    return {
8
+        'host': conf.get('HOST', 'localhost'),
9
+        'port': conf.get('PORT', 6379),
10
+        'password': '{0}:{1}'.format(conf.get('USER', ''), conf.get('PASSWORD', '')) if conf.get('USER') else '',
11
+        'db': conf.get('db', 0),
12
+    }
13
+
14
+
15
+def redis_connect(conf):
16
+    return redis.StrictRedisExtensions(connection_pool=redis.ConnectionPool(**redis_conf(conf)))

+ 43 - 0
course/local_settings_bak.py

@@ -0,0 +1,43 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import os
4
+
5
+
6
+# DEBUG = False
7
+
8
+ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
9
+
10
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11
+PROJ_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
12
+
13
+TEMPLATES = [
14
+    {
15
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
16
+        'DIRS': [os.path.join(BASE_DIR, 'templates')],
17
+        # 'APP_DIRS': True,
18
+        'OPTIONS': {
19
+            'context_processors': [
20
+                'django.template.context_processors.debug',
21
+                'django.template.context_processors.request',
22
+                'django.contrib.auth.context_processors.auth',
23
+                'django.contrib.messages.context_processors.messages',
24
+            ],
25
+            'loaders': [
26
+                'django.template.loaders.filesystem.Loader',
27
+                'django.template.loaders.app_directories.Loader',
28
+            ],
29
+        },
30
+    },
31
+]
32
+
33
+# DOMAIN
34
+DOMAIN = 'http://a.com'
35
+
36
+# 邮件设置
37
+# 只有当 DEBUG = False 的时候,才会邮件发送报错信息
38
+SERVER_EMAIL = 'error.notify@exmail.com'
39
+EMAIL_HOST_USER = 'error.notify@exmail.com'
40
+EMAIL_HOST_PASSWORD = '<^_^>pwd<^_^>'
41
+DEFAULT_FROM_EMAIL = 'error.notify <error.notify@exmail.com>'
42
+ADMINS = [('Zhang San', 'san.zhang@exmail.com'), ('Li Si', 'si.li@exmail.com')]
43
+EMAIL_SUBJECT_PREFIX = u'[Templet] '

+ 33 - 0
course/oauth_settings.py

@@ -0,0 +1,33 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+def DJANGO_WE_CFG_FUNC(request, state=None):
5
+    """ WeChat CFG Callback Func """
6
+
7
+
8
+def DJANGO_WE_BASE_FUNC(code, state, access_info=None):
9
+    """ WeChat Base Redirect Callback Func """
10
+
11
+
12
+def DJANGO_WE_USERINFO_FUNC(code, state, access_info=None, userinfo=None):
13
+    """ WeChat Userinfo Redirect Callback Func """
14
+    from account.models import UserInfo
15
+    from django.conf import settings
16
+    from utils.redis.connect import r
17
+
18
+    # Save profile or something else
19
+    unique_identifier = userinfo.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
20
+
21
+    user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
22
+    user.unionid = userinfo.get('unionid', '')
23
+    user.openid = userinfo.get('openid', '')
24
+    user.nickname = userinfo.get('nickname', '')
25
+    user.avatar = userinfo.get('headimgurl', '')
26
+    user.save()
27
+
28
+    token_check_key = user.user_id
29
+
30
+    return {
31
+        settings.TOKEN_CHECK_KEY: token_check_key,
32
+        'vtoken': r.token(token_check_key, ex=False, buf=False),
33
+    }

+ 266 - 0
course/settings.py

@@ -0,0 +1,266 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+"""
4
+Django settings for course project.
5
+
6
+Generated by 'django-admin startproject' using Django 1.11.3.
7
+
8
+For more information on this file, see
9
+https://docs.djangoproject.com/en/1.11/topics/settings/
10
+
11
+For the full list of settings and their values, see
12
+https://docs.djangoproject.com/en/1.11/ref/settings/
13
+"""
14
+
15
+import os
16
+
17
+
18
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
19
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20
+PROJ_DIR = os.path.abspath(os.path.dirname(os.path.abspath(__file__)))
21
+
22
+
23
+# Quick-start development settings - unsuitable for production
24
+# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
25
+
26
+# SECURITY WARNING: keep the secret key used in production secret!
27
+SECRET_KEY = 'b*_bo@vg6jv1!7lhfci-)7^155c+%83y69zgf6)@s^9=_32edi'
28
+
29
+# SECURITY WARNING: don't run with debug turned on in production!
30
+DEBUG = True
31
+
32
+ALLOWED_HOSTS = []
33
+
34
+
35
+# Application definition
36
+
37
+INSTALLED_APPS = [
38
+    'django.contrib.admin',
39
+    'django.contrib.auth',
40
+    'django.contrib.contenttypes',
41
+    'django.contrib.sessions',
42
+    'django.contrib.messages',
43
+    'django.contrib.staticfiles',
44
+    'django_uniapi',
45
+    'django_we',
46
+    'account',
47
+    'api',
48
+    'codes',
49
+    'courses',
50
+    'page',
51
+]
52
+
53
+MIDDLEWARE = [
54
+    'django.middleware.security.SecurityMiddleware',
55
+    'django.contrib.sessions.middleware.SessionMiddleware',
56
+    'django.middleware.common.CommonMiddleware',
57
+    # 'django.middleware.csrf.CsrfViewMiddleware',
58
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
59
+    'django.contrib.messages.middleware.MessageMiddleware',
60
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
61
+    'detect.middleware.UserAgentDetectionMiddleware',
62
+]
63
+
64
+ROOT_URLCONF = 'course.urls'
65
+
66
+TEMPLATES = [
67
+    {
68
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
69
+        'DIRS': [os.path.join(BASE_DIR, 'templates')],
70
+        # 'APP_DIRS': True,
71
+        'OPTIONS': {
72
+            'context_processors': [
73
+                'django.template.context_processors.debug',
74
+                'django.template.context_processors.request',
75
+                'django.contrib.auth.context_processors.auth',
76
+                'django.contrib.messages.context_processors.messages',
77
+            ],
78
+            'loaders': [
79
+                ('django.template.loaders.cached.Loader', [
80
+                    'django.template.loaders.filesystem.Loader',
81
+                    'django.template.loaders.app_directories.Loader',
82
+                ]),
83
+            ],
84
+        },
85
+    },
86
+]
87
+
88
+WSGI_APPLICATION = 'course.wsgi.application'
89
+
90
+
91
+# Database
92
+# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
93
+
94
+DATABASES = {
95
+    'default': {
96
+        'ENGINE': 'django.db.backends.mysql',
97
+        'NAME': 'course',
98
+        'USER': 'root',
99
+        'PASSWORD': '',
100
+        'CONN_MAX_AGE': 600,
101
+        'OPTIONS': {
102
+            # Utf8mb4 for Emoji
103
+            #
104
+            # Nickname
105
+            #
106
+            # account.WechatInfo ==> nickname
107
+            #   ALTER TABLE account_wechatinfo MODIFY COLUMN nickname VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
108
+            'charset': 'utf8mb4',
109
+        },
110
+    }
111
+}
112
+
113
+
114
+# Password validation
115
+# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
116
+
117
+AUTH_PASSWORD_VALIDATORS = [
118
+    {
119
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
120
+    },
121
+    {
122
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
123
+    },
124
+    {
125
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
126
+    },
127
+    {
128
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
129
+    },
130
+]
131
+
132
+
133
+# Internationalization
134
+# https://docs.djangoproject.com/en/1.11/topics/i18n/
135
+
136
+LANGUAGE_CODE = 'zh-Hans'
137
+
138
+TIME_ZONE = 'Asia/Shanghai'
139
+
140
+USE_I18N = True
141
+
142
+USE_L10N = True
143
+
144
+USE_TZ = True
145
+
146
+
147
+# Static files (CSS, JavaScript, Images)
148
+# https://docs.djangoproject.com/en/1.11/howto/static-files/
149
+
150
+STATICFILES_DIRS = (
151
+    os.path.join(PROJ_DIR, 'static').replace('\\', '/'),
152
+)
153
+
154
+STATIC_ROOT = os.path.join(BASE_DIR, 'collect_static').replace('\\', '/')
155
+
156
+STATIC_URL = '/static/'
157
+
158
+STATICFILES_FINDERS = (
159
+    'django.contrib.staticfiles.finders.FileSystemFinder',
160
+    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
161
+    # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
162
+)
163
+
164
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/')
165
+
166
+MEDIA_URL = '/media/'
167
+
168
+# DOMAIN
169
+DOMAIN = 'http://a.com'
170
+
171
+# Redis 设置
172
+REDIS = {
173
+    'default': {
174
+        'HOST': '127.0.0.1',
175
+        'PORT': 6379,
176
+        'USER': '',
177
+        'PASSWORD': '',
178
+        'db': 0,
179
+    }
180
+}
181
+
182
+# 微信设置
183
+WECHAT = {
184
+    'JSAPI': {
185
+        'token': '5201314',
186
+        'appID': '',
187
+        'appsecret': '',
188
+        'mchID': '',
189
+        'apiKey': '',
190
+        'mch_cert': '',
191
+        'mch_key': '',
192
+        'redpack': {
193
+
194
+        }
195
+    },
196
+}
197
+
198
+# 微信唯一标识
199
+# Choices: 'unionid' or 'openid'
200
+#
201
+# models.py
202
+#   'unique_identifier': self.unionid if settings.WECHAT_UNIQUE_IDENTIFICATION == 'unionid' else self.openid,
203
+# views.py
204
+#   unique_identifier = request.POST.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
205
+#   profile = Profile.objects.get(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
206
+WECHAT_UNIQUE_IDENTIFICATION = 'unionid'
207
+
208
+# Token 错误重授权设置
209
+TOKEN_CHECK_KEY = 'user_id'
210
+WECHAT_OAUTH2_REDIRECT_ENTRY = ''
211
+WECHAT_OAUTH2_REDIRECT_URL = ''
212
+
213
+# 错误信息邮件设置
214
+# Email address that error messages come from.
215
+SERVER_EMAIL = 'kimi@pai.ai'
216
+# The email backend to use. For possible shortcuts see django.core.mail.
217
+# The default is to use the SMTP backend.
218
+# Third-party backends can be specified by providing a Python path
219
+# to a module that defines an EmailBackend class.
220
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
221
+# Host for sending email.
222
+EMAIL_HOST = 'smtp.exmail.qq.com'
223
+# Port for sending email.
224
+EMAIL_PORT = 25
225
+# Optional SMTP authentication information for EMAIL_HOST.
226
+EMAIL_HOST_USER = 'kimi@pai.ai'
227
+EMAIL_HOST_PASSWORD = '<^_^>pwd<^_^>'
228
+EMAIL_USE_TLS = False
229
+EMAIL_USE_SSL = False
230
+EMAIL_SSL_CERTFILE = None
231
+EMAIL_SSL_KEYFILE = None
232
+EMAIL_TIMEOUT = None
233
+# Default email address to use for various automated correspondence from
234
+# the site managers.
235
+DEFAULT_FROM_EMAIL = 'Kimi <kimi@pai.ai>'
236
+# People who get code error notifications.
237
+# In the format [('Full Name', 'email@example.com'), ('Full Name', 'anotheremail@example.com')]
238
+ADMINS = [('Kimi', 'kimi@pai.ai')]
239
+# Not-necessarily-technical managers of the site. They get broken link
240
+# notifications and other various emails.
241
+MANAGERS = ADMINS
242
+# Subject-line prefix for email messages send with django.core.mail.mail_admins
243
+# or ...mail_managers.  Make sure to include the trailing space.
244
+EMAIL_SUBJECT_PREFIX = u'[Course] '
245
+
246
+# Admin Settings
247
+DISABLE_ACTION = False
248
+
249
+try:
250
+    from local_settings import *
251
+except ImportError:
252
+    pass
253
+
254
+# 依赖 local_settings 中的配置
255
+# 微信授权设置
256
+WECHAT_OAUTH2_REDIRECT_URI = '{0}/we/oauth2?scope={{0}}redirect_url={{1}}'.format(DOMAIN)
257
+WECHAT_BASE_REDIRECT_URI = '{0}/we/base_redirect'.format(DOMAIN)
258
+WECHAT_USERINFO_REDIRECT_URI = '{0}/we/userinfo_redirect'.format(DOMAIN)
259
+WECHAT_DIRECT_BASE_REDIRECT_URI = '{0}/we/direct_base_redirect'.format(DOMAIN)
260
+WECHAT_DIRECT_USERINFO_REDIRECT_URI = '{0}/we/direct_userinfo_redirect'.format(DOMAIN)
261
+
262
+try:
263
+    from func_settings import redis_connect
264
+    REDIS_CACHE = redis_connect(REDIS.get('default', {}))
265
+except ImportError:
266
+    REDIS_CACHE = None

+ 331 - 0
course/static/course/js/jswe-0.0.1.js

@@ -0,0 +1,331 @@
1
+!(function(e, t) {
2
+    var config = {
3
+        wxconfig: 'http://api.pai.ai/wx/jsapi_signature',
4
+        callback: 'callback'
5
+    }, wxData = {
6
+        debug: false,
7
+        imgUrl: '',
8
+        link: '',
9
+        desc: '',
10
+        title: '',
11
+        timeLine: ''
12
+    }, wxConfig = {
13
+        hide: false,
14
+        close: false
15
+    }, jsApiList = [
16
+        'checkJsApi',
17
+        'onMenuShareTimeline',
18
+        'onMenuShareAppMessage',
19
+        'onMenuShareQQ',
20
+        'onMenuShareWeibo',
21
+        'hideMenuItems',
22
+        'showMenuItems',
23
+        'hideAllNonBaseMenuItem',
24
+        'showAllNonBaseMenuItem',
25
+        'translateVoice',
26
+        'startRecord',
27
+        'stopRecord',
28
+        'onRecordEnd',
29
+        'playVoice',
30
+        'pauseVoice',
31
+        'stopVoice',
32
+        'uploadVoice',
33
+        'downloadVoice',
34
+        'chooseImage',
35
+        'previewImage',
36
+        'uploadImage',
37
+        'downloadImage',
38
+        'getNetworkType',
39
+        'openLocation',
40
+        'getLocation',
41
+        'hideOptionMenu',
42
+        'showOptionMenu',
43
+        'closeWindow',
44
+        'scanQRCode',
45
+        'chooseWXPay',
46
+        'openEnterpriseRedPacket',
47
+        'openProductSpecificView',
48
+        'addCard',
49
+        'chooseCard',
50
+        'openCard'
51
+    ], wxApiFun
52
+
53
+    function isOpenOnPC() {  // 判断当前网页是否在 PC 浏览器中打开
54
+        var ua = navigator.userAgent
55
+        return /windows nt/i.test(ua) || /macintosh/i.test(ua) || /linux x86_64/i.test(ua)
56
+    }
57
+
58
+    function isOpenInWeixin() {  // 判断当前网页是否在微信内置浏览器中打开
59
+        return /micromessenger/i.test(navigator.userAgent)
60
+    }
61
+
62
+    function getWeixinVersion() {
63
+        var ua = navigator.userAgent,
64
+            mt = ua.match(/micromessenger\/([\d.]+)/i)
65
+        return (mt ? mt[1] : '')
66
+    }
67
+
68
+    // This function checks whether Wechat is the appointed version or not
69
+    // Cmp: http://jsperf.com/regexp-test-vs-indexof-ignore-upper-and-lower
70
+    function isWeixinVersion(version) {
71
+        // return new RegExp('micromessenger/' + version , 'i').test(navigator.userAgent)
72
+        return navigator.userAgent.toLowerCase().indexOf('micromessenger/' + version) != -1
73
+    }
74
+
75
+    function hideOptionMenu() {
76
+        wxConfig.hide = true
77
+        fixedWxData()
78
+    }
79
+
80
+    function showOptionMenu() {
81
+        wxConfig.hide = false
82
+        fixedWxData()
83
+    }
84
+
85
+    function closeWindow() {
86
+        wxConfig.close = true
87
+        fixedWxData()
88
+    }
89
+
90
+    function wxReady(data) {
91
+        data = typeof data === 'object' ? data : JSON.parse(data)
92
+        wx.config({
93
+            debug: wxData.debug,
94
+            appId: data.appId,
95
+            timestamp: data.timestamp,
96
+            nonceStr: data.nonceStr,
97
+            signature: data.signature,
98
+            jsApiList: jsApiList
99
+        })
100
+
101
+        var callbacks = {
102
+            trigger: function (res) {
103
+                // alert('用户点击发送给朋友')
104
+                if (JSWE.wxTrigger) {JSWE.wxTrigger(res)}
105
+            },
106
+            success: function (res) {
107
+                // alert('已分享')
108
+                if (JSWE.wxSuccess) {JSWE.wxSuccess(res)}
109
+            },
110
+            cancel: function (res) {
111
+                // alert('已取消')
112
+                if (JSWE.wxCancel) {JSWE.wxCancel(res)}
113
+            },
114
+            fail: function (res) {
115
+                // alert(JSON.stringify(res))
116
+                if (JSWE.wxFail) {JSWE.wxFail(res)}
117
+            }
118
+        }, shareInfo = function(flag) {
119
+            var _share = {
120
+                title: flag ? wxData.title : (wxData.timeLine || wxData.desc),
121
+                link: wxData.link,
122
+                imgUrl: wxData.imgUrl,
123
+                trigger: callbacks.trigger,
124
+                success: callbacks.success,
125
+                cancel: callbacks.cancel,
126
+                fail: callbacks.fail
127
+            }
128
+            if (flag) _share.desc = wxData.desc
129
+            return _share
130
+        }, wxShareApi = function() {
131
+            // 2. 分享接口
132
+            // 2.1 监听“分享给朋友”,按钮点击、自定义分享内容及分享结果接口
133
+            wx.onMenuShareAppMessage(shareInfo(1))
134
+            // 2.2 监听“分享到朋友圈”按钮点击、自定义分享内容及分享结果接口
135
+            wx.onMenuShareTimeline(shareInfo(0))
136
+            // 2.3 监听“分享到QQ”按钮点击、自定义分享内容及分享结果接口
137
+            wx.onMenuShareQQ(shareInfo(1))
138
+            // 2.4 监听“分享到微博”按钮点击、自定义分享内容及分享结果接口
139
+            wx.onMenuShareWeibo(shareInfo(1))
140
+        }, wxMenuApi = function () {
141
+            // 8. 界面操作接口
142
+            // 8.1 隐藏右上角菜单
143
+            // 8.2 显示右上角菜单
144
+            if (wxConfig.hide) {wx.hideOptionMenu()} else {wx.showOptionMenu()}
145
+            // 8.7 关闭当前窗口
146
+            if (wxConfig.close) {wx.closeWindow()}
147
+        }, wxApi = function () {
148
+            wxShareApi()
149
+            wxMenuApi()
150
+        }
151
+
152
+        wx.ready(wxApi)
153
+
154
+        return wxApiFun = wxApi
155
+    }
156
+
157
+    if (isOpenInWeixin() || isOpenOnPC()) {
158
+        if ('undefined' !== typeof JSWE_CONF_UPDATE) JSWE_CONF_UPDATE(config)
159
+        $.ajax({
160
+            url: config.wxconfig,
161
+            type: 'get',
162
+            dataType: 'jsonp',
163
+            jsonpCallback: config.callback,
164
+            data: {
165
+                url: window.location.href.split('#')[0]
166
+            },
167
+            success: wxReady
168
+        })
169
+    }
170
+
171
+    function initWxData(data, flag) {
172
+        for(var d in data) {if (d in wxData) wxData[d] = data[d]}
173
+        if (flag) fixedWxData()
174
+    }
175
+
176
+    function changeWxData(key, value, flag) {
177
+        if (key in falDwxDataata) {wxData[key] = value}
178
+        if (flag) fixedWxData()
179
+    }
180
+
181
+    function fixedWxData() {
182
+        if ('undefined' !== typeof wxApiFun) wxApiFun()
183
+    }
184
+
185
+    // 5 图片接口
186
+    // 5.1 拍照、本地选图
187
+    var images = {
188
+        localIds: [],
189
+        serverIds: []
190
+    };
191
+    // function chooseImage(count, directUpload, isShowProgressTips) {
192
+    function chooseImage(choose_params) {
193
+        if ('undefined' === typeof choose_params) choose_params = {}
194
+        wx.chooseImage({
195
+            count: choose_params.count || 9, // 默认9
196
+            sizeType: choose_params.sizeType || ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
197
+            sourceType: choose_params.sourceType || ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
198
+            success: function (res) {
199
+                images.localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
200
+                // 判断是否直接上传
201
+                if (choose_params.directUpload) {setTimeout(uploadImages({localIds: images.localIds, isShowProgressTips: choose_params.isShowProgressTips || 1}), 100)}
202
+                // 拍照、本地选图成功后的回调函数
203
+                if (JSWE.wxChooseImageSuccess) {JSWE.wxChooseImageSuccess(res)}
204
+            }
205
+        });
206
+    }
207
+
208
+    // 5.2 图片预览
209
+    function previewImage(preview_params) {
210
+        wx.previewImage({
211
+            current: preview_params.current, // 当前显示图片的链接,不填则默认为 urls 的第一张
212
+            urls: preview_params.urls // 需要预览的图片链接列表
213
+        });
214
+    }
215
+
216
+    // 5.3 上传图片
217
+    // function uploadImage(localId, isShowProgressTips) {
218
+    function uploadImage(upload_params) {
219
+        // 上传图片为异步处理,重复上传同一图片,返回的serverId也是不同的
220
+        wx.uploadImage({
221
+            localId: upload_params.localId, // 需要上传的图片的本地ID,由chooseImage接口获得
222
+            isShowProgressTips: upload_params.isShowProgressTips || 1, // 默认为1,显示进度提示
223
+            success: function (res) {
224
+                images.serverIds.push(res.serverId); // 返回图片的服务器端ID
225
+                // 上传图片成功后的回调函数
226
+                if (JSWE.wxUploadImageSuccess) {JSWE.wxUploadImageSuccess(res)}
227
+            }
228
+        });
229
+    }
230
+
231
+    // function uploadImages(localIds, isShowProgressTips) {
232
+    function uploadImages(upload_params) {
233
+        var localIds = upload_params.localIds, isShowProgressTips = upload_params.isShowProgressTips || 1
234
+        images.serverIds = [];
235
+        for (var idx in localIds) {uploadImage({localId: localIds[idx], isShowProgressTips: isShowProgressTips})}
236
+    }
237
+
238
+    // 9 微信原生接口
239
+    // 9.1.1 扫描二维码并返回结果
240
+    // 9.1.2 扫描二维码并返回结果
241
+    function scanQRCode(scan_params) {
242
+        if ('undefined' === typeof scan_params) scan_params = {}
243
+        wx.scanQRCode({
244
+            needResult: scan_params.needResult || 0,  // 默认为0,0扫描结果由微信处理,1直接返回扫描结果
245
+            scanType: scan_params.scanType || ['qrCode', 'barCode'],  // 可以指定扫二维码还是一维码,默认二者都有
246
+            success: function (res) {  // 当 needResult 为 1 时,扫码返回的结果
247
+                if (JSWE.wxScanQRCodeSuccess) {JSWE.wxScanQRCodeSuccess(res)}
248
+            }
249
+        });
250
+    }
251
+
252
+    // QRCode & BarCode is different
253
+    function parseScanQRCodeResultStr(resultStr) {
254
+        var strs = resultStr.split(',')
255
+        return strs[strs.length - 1]
256
+    }
257
+
258
+    // 10 微信支付接口
259
+    // 10.1 发起一个支付请求
260
+    function chooseWXPay(wxpay_params) {
261
+        wx.chooseWXPay({
262
+            timestamp: wxpay_params.timeStamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
263
+            nonceStr: wxpay_params.nonceStr, // 支付签名随机串,不长于 32 位
264
+            package: wxpay_params.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
265
+            signType: wxpay_params.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
266
+            paySign: wxpay_params.paySign, // 支付签名
267
+            success: function (res) {
268
+                // 支付成功后的回调函数
269
+                if (JSWE.wxPaySuccess) {JSWE.wxPaySuccess(res)}
270
+            }
271
+        })
272
+    }
273
+
274
+    // xx 微信原生企业红包接口
275
+    // xx.1 发起一个发送原生企业红包请求
276
+    function openEnterpriseRedPacket(wxredpack_params) {
277
+        wx.openEnterpriseRedPacket({
278
+            timeStamp: wxredpack_params.timeStamp, // 红包签名时间戳,注意原生企业红包接口timeStamp字段名需大写其中的S字符,而支付接口timeStamp字段名无需大写其中的S字符。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
279
+            nonceStr: wxredpack_params.nonceStr, // 红包签名随机串,不长于 32 位
280
+            package: encodeURIComponent(wxredpack_params.package), // 发放红包接口返回的prepay_id参数值,提交格式如:prepay_id=***)
281
+            signType: wxredpack_params.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
282
+            paySign: wxredpack_params.paySign, // 红包签名
283
+            success: function (res) {
284
+                // 发送原生企业红包成功后的回调函数
285
+                if (JSWE.wxEnterpriseRedPacketSuccess) {JSWE.wxEnterpriseRedPacketSuccess(res)}
286
+            }
287
+        })
288
+    }
289
+
290
+    var v = {
291
+        version: '1.0.5',
292
+
293
+        // Basic Vars
294
+        config: config,
295
+        wxData: wxData,
296
+        jsApiList: jsApiList,
297
+
298
+        // Weixin Function
299
+        isOpenInWeixin: isOpenInWeixin,
300
+        getWeixinVersion: getWeixinVersion,
301
+        isWeixinVersion: isWeixinVersion,
302
+
303
+        // Menu Function
304
+        hideOptionMenu: hideOptionMenu,
305
+        showOptionMenu: showOptionMenu,
306
+        closeWindow: closeWindow,
307
+
308
+        // Share Function
309
+        initWxData: initWxData,
310
+        changeWxData: changeWxData,
311
+        fixedWxData: fixedWxData,
312
+
313
+        // Image Function
314
+        images: images,
315
+        chooseImage: chooseImage,
316
+        previewImage: previewImage,
317
+        uploadImage: uploadImage,
318
+        uploadImages: uploadImages,
319
+
320
+        // Scan Function
321
+        scanQRCode: scanQRCode,
322
+        parseScanQRCodeResultStr: parseScanQRCodeResultStr,
323
+
324
+        // Pay Function
325
+        chooseWXPay: chooseWXPay,
326
+
327
+        // EnterpriseRedPacket Function
328
+        openEnterpriseRedPacket: openEnterpriseRedPacket
329
+    }
330
+    e.JSWE = e.V = v
331
+})(window)

+ 36 - 0
course/urls.py

@@ -0,0 +1,36 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+"""course URL Configuration
4
+
5
+The `urlpatterns` list routes URLs to views. For more information please see:
6
+    https://docs.djangoproject.com/en/1.11/topics/http/urls/
7
+Examples:
8
+Function views
9
+    1. Add an import:  from my_app import views
10
+    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
11
+Class-based views
12
+    1. Add an import:  from other_app.views import Home
13
+    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
14
+Including another URLconf
15
+    1. Import the include() function: from django.conf.urls import url, include
16
+    2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
17
+"""
18
+from django.conf import settings
19
+from django.conf.urls import include, url
20
+from django.conf.urls.static import static
21
+from django.contrib import admin
22
+
23
+
24
+urlpatterns = [
25
+    url(r'^courseadmin/', admin.site.urls),
26
+    url(r'^api/', include('api.urls', namespace='api')),
27
+    url(r'^uniapi/', include('django_uniapi.urls', namespace='uniapi')),
28
+    url(r'^we/', include('django_we.urls', namespace='wechat')),
29
+
30
+    url(r'^page/', include('page.urls', namespace='page')),
31
+]
32
+
33
+urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
34
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
35
+
36
+admin.site.site_header = u'合作课程系统'

+ 17 - 0
course/wsgi.py

@@ -0,0 +1,17 @@
1
+"""
2
+WSGI config for course project.
3
+
4
+It exposes the WSGI callable as a module-level variable named ``application``.
5
+
6
+For more information on this file, see
7
+https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
8
+"""
9
+
10
+import os
11
+
12
+from django.core.wsgi import get_wsgi_application
13
+
14
+
15
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course.settings")
16
+
17
+application = get_wsgi_application()

+ 0 - 0
courses/__init__.py


+ 20 - 0
courses/admin.py

@@ -0,0 +1,20 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from djadmin import ExportExcelModelAdmin, ReadonlyModelAdmin
4
+from django.contrib import admin
5
+
6
+from courses.models import CourseInfo, CourseVideoInfo
7
+
8
+
9
+class CourseInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin):
10
+    list_display = ('course_id', 'course_name', 'course_time', 'course_cover', 'status', 'created_at', 'updated_at')
11
+    list_filter = ('status', )
12
+
13
+
14
+class CourseVideoInfoAdmin(ExportExcelModelAdmin, admin.ModelAdmin):
15
+    list_display = ('course', 'course_video_id', 'course_video_type', 'course_video_name', 'course_video_desc', 'course_video_time', 'course_video_cover', 'course_video_position', 'status', 'created_at', 'updated_at')
16
+    list_filter = ('course', 'status')
17
+
18
+
19
+admin.site.register(CourseInfo, CourseInfoAdmin)
20
+admin.site.register(CourseVideoInfo, CourseVideoInfoAdmin)

+ 8 - 0
courses/apps.py

@@ -0,0 +1,8 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.apps import AppConfig
5
+
6
+
7
+class CoursesConfig(AppConfig):
8
+    name = 'courses'

+ 57 - 0
courses/migrations/0001_initial.py

@@ -0,0 +1,57 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.11.3 on 2017-09-25 07:47
3
+from __future__ import unicode_literals
4
+
5
+import courses.models
6
+from django.db import migrations, models
7
+import django.db.models.deletion
8
+import shortuuidfield.fields
9
+
10
+
11
+class Migration(migrations.Migration):
12
+
13
+    initial = True
14
+
15
+    dependencies = [
16
+    ]
17
+
18
+    operations = [
19
+        migrations.CreateModel(
20
+            name='CourseInfo',
21
+            fields=[
22
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23
+                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
24
+                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
25
+                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
26
+                ('course_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u8bfe\u7a0b\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
27
+                ('course_name', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u6807\u9898', max_length=255, null=True, verbose_name='course_name')),
28
+                ('course_time', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u65f6\u95f4', verbose_name='course_time')),
29
+                ('course_cover', models.ImageField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7f29\u7565\u56fe', null=True, upload_to=courses.models.upload_path, verbose_name='course_cover')),
30
+            ],
31
+            options={
32
+                'verbose_name': 'courseinfo',
33
+                'verbose_name_plural': 'courseinfo',
34
+            },
35
+        ),
36
+        migrations.CreateModel(
37
+            name='CourseVideoInfo',
38
+            fields=[
39
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
40
+                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
41
+                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
42
+                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
43
+                ('course_video_id', shortuuidfield.fields.ShortUUIDField(blank=True, db_index=True, editable=False, help_text='\u8bfe\u7a0b\u89c6\u9891\u552f\u4e00\u6807\u8bc6', max_length=22, unique=True)),
44
+                ('course_video_type', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7c7b\u578b', max_length=255, null=True, verbose_name='course_video_type')),
45
+                ('course_video_name', models.CharField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u6807\u9898', max_length=255, null=True, verbose_name='course_video_name')),
46
+                ('course_video_desc', models.TextField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u63cf\u8ff0', null=True, verbose_name='course_video_desc')),
47
+                ('course_video_time', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u89c6\u9891\u65f6\u95f4', verbose_name='course_video_time')),
48
+                ('course_video_cover', models.ImageField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891\u7f29\u7565\u56fe', null=True, upload_to=courses.models.upload_path, verbose_name='course_video_cover')),
49
+                ('course_video_position', models.IntegerField(default=0, help_text='\u8bfe\u7a0b\u89c6\u9891\u6392\u5e8f', verbose_name='course_video_position')),
50
+                ('course', models.ForeignKey(blank=True, help_text='\u8bfe\u7a0b', null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.CourseInfo', verbose_name='course')),
51
+            ],
52
+            options={
53
+                'verbose_name': 'coursevideoinfo',
54
+                'verbose_name_plural': 'coursevideoinfo',
55
+            },
56
+        ),
57
+    ]

+ 21 - 0
courses/migrations/0002_coursevideoinfo_course_video.py

@@ -0,0 +1,21 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.11.3 on 2017-09-25 08:55
3
+from __future__ import unicode_literals
4
+
5
+import courses.models
6
+from django.db import migrations, models
7
+
8
+
9
+class Migration(migrations.Migration):
10
+
11
+    dependencies = [
12
+        ('courses', '0001_initial'),
13
+    ]
14
+
15
+    operations = [
16
+        migrations.AddField(
17
+            model_name='coursevideoinfo',
18
+            name='course_video',
19
+            field=models.FileField(blank=True, help_text='\u8bfe\u7a0b\u89c6\u9891', null=True, upload_to=courses.models.upload_path, verbose_name='course_video'),
20
+        ),
21
+    ]

+ 0 - 0
courses/migrations/__init__.py


+ 87 - 0
courses/models.py

@@ -0,0 +1,87 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import os
4
+
5
+from django.db import models
6
+from django.utils.translation import ugettext_lazy as _
7
+from shortuuidfield import ShortUUIDField
8
+from TimeConvert import TimeConvert as tc
9
+
10
+from course.basemodels import CreateUpdateMixin
11
+from utils.url_utils import upload_file_url
12
+
13
+
14
+def upload_path(instance, old_filename):
15
+    return 'file/{ym}/{stamp}{ext}'.format(
16
+        ym=tc.local_string(format='%Y%m'),
17
+        stamp=tc.local_timestamp(ms=True),
18
+        ext=os.path.splitext(old_filename)[1].lower(),
19
+    )
20
+
21
+
22
+class CourseInfo(CreateUpdateMixin):
23
+    course_id = ShortUUIDField(_(u'course_id'), max_length=255, help_text=u'课程唯一标识', db_index=True, unique=True)
24
+    course_name = models.CharField(_(u'course_name'), max_length=255, blank=True, null=True, help_text=u'课程标题')
25
+    course_time = models.IntegerField(_(u'course_time'), default=0, help_text=u'课程时间')
26
+    course_cover = models.ImageField(_(u'course_cover'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频缩略图')
27
+
28
+    class Meta:
29
+        verbose_name = _(u'courseinfo')
30
+        verbose_name_plural = _(u'courseinfo')
31
+
32
+    def __unicode__(self):
33
+        return unicode(self.course_name)
34
+
35
+    @property
36
+    def course_cover_url(self):
37
+        return upload_file_url(self.course_cover)
38
+
39
+    @property
40
+    def data(self):
41
+        return {
42
+            'course_id': self.course_id,
43
+            'course_name': self.course_name,
44
+            'course_time': self.course_time,
45
+            'course_cover_url': self.course_cover_url,
46
+        }
47
+
48
+
49
+class CourseVideoInfo(CreateUpdateMixin):
50
+    course = models.ForeignKey(CourseInfo, verbose_name=_(u'course'), blank=True, null=True, help_text=u'课程', db_index=True)
51
+    course_video_id = ShortUUIDField(_(u'course_video_id'), max_length=255, help_text=u'课程视频唯一标识', db_index=True, unique=True)
52
+    course_video_type = models.CharField(_(u'course_video_type'), max_length=255, blank=True, null=True, help_text=u'课程视频类型')
53
+    course_video_name = models.CharField(_(u'course_video_name'), max_length=255, blank=True, null=True, help_text=u'课程视频标题')
54
+    course_video_desc = models.TextField(_(u'course_video_desc'), blank=True, null=True, help_text=u'课程视频描述')
55
+    course_video_time = models.IntegerField(_(u'course_video_time'), default=0, help_text=u'课程视频时间')
56
+    course_video_cover = models.ImageField(_(u'course_video_cover'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频缩略图')
57
+    course_video = models.FileField(_(u'course_video'), upload_to=upload_path, blank=True, null=True, help_text=u'课程视频')
58
+    course_video_position = models.IntegerField(_(u'course_video_position'), default=0, help_text=u'课程视频排序')
59
+
60
+    class Meta:
61
+        verbose_name = _(u'coursevideoinfo')
62
+        verbose_name_plural = _(u'coursevideoinfo')
63
+
64
+    def __unicode__(self):
65
+        return unicode(self.pk)
66
+
67
+    @property
68
+    def course_video_cover_url(self):
69
+        return upload_file_url(self.course_video_cover)
70
+
71
+    @property
72
+    def course_video_url(self):
73
+        return upload_file_url(self.course_video)
74
+
75
+    @property
76
+    def data(self):
77
+        return {
78
+            'course_id': self.course.course_id,
79
+            'course_video_id': self.course_video_id,
80
+            'course_video_type': self.course_video_type,
81
+            'course_video_name': self.course_video_name,
82
+            'course_video_desc': self.course_video_desc,
83
+            'course_video_time': self.course_video_time,
84
+            'course_video_cover_url': self.course_video_cover_url,
85
+            'course_video_url': self.course_video_url,
86
+            'course_video_position': self.course_video_position,
87
+        }

+ 7 - 0
courses/tests.py

@@ -0,0 +1,7 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.test import TestCase
5
+
6
+
7
+# Create your tests here.

+ 7 - 0
courses/views.py

@@ -0,0 +1,7 @@
1
+# -*- coding: utf-8 -*-
2
+from __future__ import unicode_literals
3
+
4
+from django.shortcuts import render
5
+
6
+
7
+# Create your views here.

+ 3 - 0
isort.sh

@@ -0,0 +1,3 @@
1
+#!/bin/bash
2
+
3
+isort -rc -sp . .

+ 23 - 0
manage.py

@@ -0,0 +1,23 @@
1
+#!/usr/bin/env python
2
+import os
3
+import sys
4
+
5
+
6
+if __name__ == "__main__":
7
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "course.settings")
8
+    try:
9
+        from django.core.management import execute_from_command_line
10
+    except ImportError:
11
+        # The above import may fail for some other reason. Ensure that the
12
+        # issue is really that Django is missing to avoid masking other
13
+        # exceptions on Python 2.
14
+        try:
15
+            import django
16
+        except ImportError:
17
+            raise ImportError(
18
+                "Couldn't import Django. Are you sure it's installed and "
19
+                "available on your PYTHONPATH environment variable? Did you "
20
+                "forget to activate a virtual environment?"
21
+            )
22
+        raise
23
+    execute_from_command_line(sys.argv)

+ 0 - 0
page/__init__.py


+ 4 - 0
page/admin.py

@@ -0,0 +1,4 @@
1
+from django.contrib import admin
2
+
3
+
4
+# Register your models here.

+ 67 - 0
page/code_views.py

@@ -0,0 +1,67 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from __future__ import division
4
+
5
+from django.conf import settings
6
+from django.core.urlresolvers import reverse
7
+from django.db import transaction
8
+from django.shortcuts import redirect, render
9
+from furl import furl
10
+
11
+from account.models import UserInfo
12
+from codes.models import CourseCodeInfo
13
+from course.decorators import check_token
14
+from utils.error.errno_utils import CourseCodeStatusCode, ProfileStatusCode
15
+from utils.error.response_utils import response
16
+
17
+
18
+@check_token
19
+@transaction.atomic
20
+def course_code(request):
21
+    unique_identifier = request.GET.get(settings.WECHAT_UNIQUE_IDENTIFICATION, '')
22
+
23
+    user, created = UserInfo.objects.select_for_update().get_or_create(**{settings.WECHAT_UNIQUE_IDENTIFICATION: unique_identifier})
24
+    user.unionid = request.GET.get('unionid', '')
25
+    user.openid = request.GET.get('openid', '')
26
+    user.nickname = request.GET.get('nickname', '')
27
+    user.avatar = request.GET.get('headimgurl', '')
28
+    user.save()
29
+
30
+    try:
31
+        course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True)
32
+    except CourseCodeInfo.DoesNotExist:
33
+        course_code = None
34
+
35
+    if course_code:
36
+        return redirect(furl(reverse('page:course_list')).add(request.GET).add({'user_id': user.user_id}).url)
37
+
38
+    return render(request, 'page/course_code.html', {
39
+        'domain': settings.DOMAIN,
40
+        'user_info': user.data,
41
+        'params': 'user_id={}&vtoken={}'.format(user.user_id, request.GET.get('vtoken', '')),
42
+    })
43
+
44
+
45
+@transaction.atomic
46
+def code_exchange(request):
47
+    user_id = request.POST.get('user_id', '')
48
+    code = request.POST.get('code', '')
49
+
50
+    try:
51
+        user = UserInfo.objects.select_for_update().get(user_id=user_id)
52
+    except UserInfo.DoesNotExist:
53
+        return response(ProfileStatusCode.PROFILE_NOT_FOUND)
54
+
55
+    try:
56
+        course_code = CourseCodeInfo.objects.select_for_update().get(code=code, status=True)
57
+    except CourseCodeInfo.DoesNotExist:
58
+        return response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND)
59
+
60
+    if course_code.exchanged:
61
+        return response(CourseCodeStatusCode.COURSE_CODE_HAS_EXCHANGED)
62
+
63
+    course_code.user_id = user.user_id
64
+    course_code.exchanged = True
65
+    course_code.save()
66
+
67
+    return response(200, 'Course Code Exchanged Success', u'课程兑换码兑换成功')

+ 47 - 0
page/info_views.py

@@ -0,0 +1,47 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from __future__ import division
4
+
5
+from django.conf import settings
6
+from django.shortcuts import render
7
+
8
+from account.models import UserInfo
9
+from codes.models import CourseCodeInfo
10
+from course.decorators import check_token
11
+from courses.models import CourseInfo, CourseVideoInfo
12
+from utils.error.errno_utils import CourseCodeStatusCode, CourseStatusCode, ProfileStatusCode
13
+from utils.error.response_utils import response
14
+
15
+
16
+@check_token
17
+def course_info(request):
18
+    user_id = request.GET.get('user_id', '')
19
+    course_id = request.GET.get('course_id', '')
20
+
21
+    try:
22
+        user = UserInfo.objects.get(user_id=user_id, status=True)
23
+    except UserInfo.DoesNotExist:
24
+        return response(ProfileStatusCode.PROFILE_NOT_FOUND)
25
+
26
+    try:
27
+        course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True)
28
+    except CourseCodeInfo.DoesNotExist:
29
+        response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND)
30
+
31
+    try:
32
+        course = CourseInfo.objects.get(course_id=course_id)
33
+    except CourseInfo.DoesNotExist:
34
+        response(CourseStatusCode.COURSE_NOT_FOUND)
35
+
36
+    videos = CourseVideoInfo.objects.filter(course=course, status=True).order_by('course_video_position')
37
+    videos = [video.data for video in videos]
38
+
39
+    video_count = len(videos)
40
+
41
+    return render(request, 'page/course_info.html', {
42
+        'domain': settings.DOMAIN,
43
+        'video_default': videos[0] if video_count else '',
44
+        'video_count': video_count,
45
+        'videos': videos,
46
+        'params': 'user_id={}&vtoken={}'.format(user_id, request.GET.get('vtoken', '')),
47
+    })

+ 37 - 0
page/list_views.py

@@ -0,0 +1,37 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from __future__ import division
4
+
5
+from django.conf import settings
6
+from django.shortcuts import render
7
+
8
+from account.models import UserInfo
9
+from codes.models import CourseCodeInfo
10
+from course.decorators import check_token
11
+from courses.models import CourseInfo
12
+from utils.error.errno_utils import CourseCodeStatusCode, ProfileStatusCode
13
+from utils.error.response_utils import response
14
+
15
+
16
+@check_token
17
+def course_list(request):
18
+    user_id = request.GET.get('user_id', '')
19
+
20
+    try:
21
+        user = UserInfo.objects.get(user_id=user_id, status=True)
22
+    except UserInfo.DoesNotExist:
23
+        return response(ProfileStatusCode.PROFILE_NOT_FOUND)
24
+
25
+    try:
26
+        course_code = CourseCodeInfo.objects.get(user_id=user.user_id, exchanged=True, status=True)
27
+    except CourseCodeInfo.DoesNotExist:
28
+        response(CourseCodeStatusCode.COURSE_CODE_NOT_FOUND)
29
+
30
+    courses = CourseInfo.objects.filter(status=True)
31
+    courses = [course.data for course in courses]
32
+
33
+    return render(request, 'page/course_list.html', {
34
+        'domain': settings.DOMAIN,
35
+        'courses': courses,
36
+        'params': 'user_id={}&vtoken={}'.format(user_id, request.GET.get('vtoken', '')),
37
+    })

+ 31 - 0
page/migrations/0001_initial.py

@@ -0,0 +1,31 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.11.3 on 2017-09-24 14:50
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+import page.models
7
+
8
+
9
+class Migration(migrations.Migration):
10
+
11
+    initial = True
12
+
13
+    dependencies = [
14
+    ]
15
+
16
+    operations = [
17
+        migrations.CreateModel(
18
+            name='CourseCodeSettingInfo',
19
+            fields=[
20
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
+                ('status', models.BooleanField(db_index=True, default=True, help_text='\u72b6\u6001', verbose_name='status')),
22
+                ('created_at', models.DateTimeField(auto_now_add=True, help_text='\u521b\u5efa\u65f6\u95f4', verbose_name='created_at')),
23
+                ('updated_at', models.DateTimeField(auto_now=True, help_text='\u66f4\u65b0\u65f6\u95f4', verbose_name='updated_at')),
24
+                ('cover_image', models.ImageField(blank=True, help_text='\u5151\u6362\u8bfe\u7a0b\u9875\u56fe\u7247', null=True, upload_to=page.models.upload_path, verbose_name='cover_image')),
25
+            ],
26
+            options={
27
+                'verbose_name': 'coursecodesettinginfo',
28
+                'verbose_name_plural': 'coursecodesettinginfo',
29
+            },
30
+        ),
31
+    ]

+ 0 - 0
page/migrations/__init__.py


+ 33 - 0
page/models.py

@@ -0,0 +1,33 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import os
4
+
5
+from django.db import models
6
+from django.utils.translation import ugettext_lazy as _
7
+from TimeConvert import TimeConvert as tc
8
+
9
+from course.basemodels import CreateUpdateMixin
10
+from utils.url_utils import upload_file_url
11
+
12
+
13
+def upload_path(instance, old_filename):
14
+    return 'file/{ym}/{stamp}{ext}'.format(
15
+        ym=tc.local_string(format='%Y%m'),
16
+        stamp=tc.local_timestamp(ms=True),
17
+        ext=os.path.splitext(old_filename)[1].lower(),
18
+    )
19
+
20
+
21
+class CourseCodeSettingInfo(CreateUpdateMixin):
22
+    cover_image = models.ImageField(_(u'cover_image'), upload_to=upload_path, blank=True, null=True, help_text=u'兑换课程页图片')
23
+
24
+    class Meta:
25
+        verbose_name = _(u'coursecodesettinginfo')
26
+        verbose_name_plural = _(u'coursecodesettinginfo')
27
+
28
+    def __unicode__(self):
29
+        return unicode(self.pk)
30
+
31
+    @property
32
+    def cover_image_url(self):
33
+        return upload_file_url(self.cover_image)

+ 67 - 0
page/static/page/css/weui.ext.css

@@ -0,0 +1,67 @@
1
+/* Input valid or invalid */
2
+input:required:invalid {
3
+    color: #E64340;
4
+}
5
+input:required:valid {
6
+    color: rgb(0, 0, 0);
7
+}
8
+/* Input Placeholder */
9
+ input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
10
+    font-size: 13px;
11
+}
12
+input:-moz-placeholder, textarea:-moz-placeholder {
13
+    font-size: 13px;
14
+}
15
+input::-moz-placeholder, textarea::-moz-placeholder {
16
+    font-size: 13px;
17
+}
18
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
19
+    font-size: 13px;
20
+}
21
+/* Radio Cells */
22
+.radio_cells {
23
+    margin-top: 0;
24
+    margin-left: 15px;
25
+}
26
+.radio_cells label {
27
+    padding: 8px 10px;
28
+    font-size: 15px;
29
+}
30
+/*.radio_cells>div:first-child .quartern:after {*/
31
+    /*border-left: none;*/
32
+/*}*/
33
+.radio_cells>div:last-child .quartern:after {
34
+    border-right: none;
35
+}
36
+/* Quartern */
37
+.quartern {
38
+    width: 25%;
39
+    box-sizing: border-box;
40
+    text-align: center;
41
+    border-radius: 5px;
42
+    float: left;
43
+}
44
+.quartern:after {
45
+    content: " ";
46
+    width: 200%;
47
+    height: 200%;
48
+    position: absolute;
49
+    top: 0;
50
+    left: 0;
51
+    border-right: 1px solid rgba(0, 0, 0, 0.2);
52
+    /*border-width: 0 1px 0 1px;*/
53
+    /*border-color: rgba(0, 0, 0, 0.2);*/
54
+    /*border-style: solid;*/
55
+    -webkit-transform: scale(0.5);
56
+            transform: scale(0.5);
57
+    -webkit-transform-origin: 0 0;
58
+            transform-origin: 0 0;
59
+    box-sizing: border-box;
60
+    border-radius: 10px;
61
+}
62
+/* Radio Checked Relative */
63
+.weui_check:checked + .quartern {
64
+    color: white;
65
+    background: #04BE02;
66
+    border-width: 0;
67
+}

BIN
page/static/page/img/code_cover.png


+ 135 - 0
page/templates/page/course_code.html

@@ -0,0 +1,135 @@
1
+{% load staticfiles %}
2
+
3
+<!DOCTYPE html>
4
+<html lang="zh-CN">
5
+    <head>
6
+        <meta charset="utf-8">
7
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
8
+        <meta name="format-detection" content="telephone=no,email=no,address=no">
9
+        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
10
+        <title>课程兑换</title>
11
+
12
+        <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" />
13
+{#        <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />#}
14
+
15
+        <style>
16
+            .code-cover, .code-cover>img {
17
+                width: 100%;
18
+            }
19
+            .code-area {
20
+                padding: 5px 10px;
21
+            }
22
+            .code-label {
23
+                width: 70%;
24
+                color: #c6c6c6;
25
+                font-size: 13px;
26
+                margin: 15px auto;
27
+            }
28
+            .code-input, .code-input>input, .code-submit {
29
+                width: 80%;
30
+                height: 40px;
31
+                line-height: 40px;
32
+                margin: 5px auto;
33
+                border-radius: 25px;
34
+                box-sizing: border-box;
35
+            }
36
+            .code-input>input {
37
+                width: 100%;
38
+                border: 1px solid #c6c6c6;
39
+                padding: 0 15px;
40
+                outline: medium;
41
+            }
42
+            .code-submit {
43
+                text-align: center;
44
+                background: #20a1f5;
45
+                color: #c6eaf9;
46
+                margin-top: 15px;
47
+            }
48
+        </style>
49
+    </head>
50
+    <body>
51
+        <div class="container">
52
+            <div class="code-cover"><img src="{% static 'page/img/code_cover.png' %}"></div>
53
+            <div class="code-area">
54
+                <div class="code-label">输入兑换码兑换课程</div>
55
+                <div class="code-input"><input id="code" placeholder="请输入兑换码"></div>
56
+                <div id="submit" class="code-submit">确认兑换</div>
57
+            </div>
58
+
59
+            <div class="weui_dialog_alert" id="dialog" style="display: none">
60
+                <div class="weui_mask"></div>
61
+                <div class="weui_dialog">
62
+                    <div class="weui_dialog_hd"><strong id="title" class="weui_dialog_title">弹窗标题</strong></div>
63
+                    <div id="content" class="weui_dialog_bd">弹窗内容,告知当前页面信息等</div>
64
+                    <div class="weui_dialog_ft">
65
+                        <a href="javascript:;" class="weui_btn_dialog primary">确定</a>
66
+                    </div>
67
+                </div>
68
+            </div>
69
+        </div>
70
+
71
+        <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script>
72
+        <script>
73
+            $(function() {
74
+                function show_error_dialog(title, content) {
75
+                    $('#dialog #title').text(title);
76
+                    $('#dialog #content').text(content);
77
+                    $('#dialog').show();
78
+                }
79
+
80
+                function data_check() {
81
+                    var user_id = '{{ user_info.user_id }}';
82
+                    if (!user_id) {
83
+                        show_error_dialog('微信授权', '微信授权失败,请重新打开页面');
84
+                        return false;
85
+                    }
86
+
87
+                    var code = $('#code').val();
88
+                    if (!code) {
89
+                        show_error_dialog('兑换码', '兑换码错误,请检查重新输入');
90
+                        return false;
91
+                    }
92
+
93
+                    return {
94
+                        user_id: user_id,
95
+                        code: code,
96
+                    }
97
+                }
98
+
99
+                $('#submit').click(function () {
100
+                    var check_result = data_check();
101
+                    if (check_result){
102
+                        $.ajax({
103
+                            type: 'POST',
104
+                            url: '{{ domain }}/api/code/exchange',
105
+                            data: check_result,
106
+                            success: function(data) {
107
+                                if (data.status == 200) {
108
+                                    window.location.href = '{{ domain }}/page/course/list?{{ params|safe }}';
109
+                                } else {
110
+                                    show_error_dialog('错误', data.description);
111
+                                }
112
+                            }
113
+                        })
114
+                    }
115
+                });
116
+
117
+                $('#dialog .weui_btn_dialog').click(function () {
118
+                    $('#dialog').hide();
119
+                })
120
+            });
121
+        </script>
122
+        <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
123
+        <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
124
+        <script>
125
+            V.initWxData({
126
+                imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png',
127
+                link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk',
128
+                desc: '店员授权',
129
+                title: '店员授权',
130
+                timeLine: ''
131
+            }, true);
132
+{#            V.hideOptionMenu();#}
133
+        </script>
134
+    </body>
135
+</html>

+ 124 - 0
page/templates/page/course_info.html

@@ -0,0 +1,124 @@
1
+{% load staticfiles %}
2
+
3
+<!DOCTYPE html>
4
+<html lang="zh-CN">
5
+    <head>
6
+        <meta charset="utf-8">
7
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
8
+        <meta name="format-detection" content="telephone=no,email=no,address=no">
9
+        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
10
+        <title>课程详情</title>
11
+
12
+        <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" />
13
+        <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />
14
+
15
+        <style>
16
+            .container {
17
+                position: absolute;
18
+                top: 0;
19
+                right: 0;
20
+                bottom: 0;
21
+                left: 0;
22
+                background: #efefef;
23
+            }
24
+            .video_wrapper, .video_select {
25
+                width: 100%;
26
+                background: #fff;
27
+                margin-bottom: 10px;
28
+                box-sizing: border-box;
29
+            }
30
+            .video_text, .video_select {
31
+                padding: 15px;
32
+            }
33
+            .course_video_name, .video_select_text {
34
+                font-size: 18px;
35
+                font-weight: bold;
36
+                color: #020001;
37
+                padding-bottom: 10px;
38
+            }
39
+            .course_video_desc {
40
+                font-size: 12px;
41
+                color: #999;
42
+            }
43
+            .video_select_item {
44
+                width: 100%;
45
+                text-align: center;
46
+                height: 40px;
47
+                line-height: 40px;
48
+                color: #020001;
49
+                border: 1px solid #e1e1e1;
50
+                border-radius: 5px;
51
+                margin-bottom: 15px;
52
+            }
53
+            .video_selected {
54
+                color: #ce8f8a !important;
55
+                border: 1px solid #ce8f8a;
56
+            }
57
+        </style>
58
+    </head>
59
+    <body>
60
+        <div class="container" >
61
+            <div class="video_wrapper">
62
+                <video id="video" width="100%" height="100%" autoplay controls x-webkit-airplay="true" webkit-playsinline="" playsinline="true" preload="none" poster="" src="{{ video_default.course_video_url }}" data-cursrc="1"></video>
63
+                <div class="video_text">
64
+                    <div class="course_video_name">{{ video_default.course_video_name }}</div>
65
+                    <div class="course_video_desc">{{ video_default.course_video_desc }}</div>
66
+                </div>
67
+            </div>
68
+
69
+            <div class="video_select">
70
+                <div class="video_select_text">选择视频</div>
71
+                {% for video in videos %}
72
+                    <div id="video{{ forloop.counter }}" class="video_select_item {% ifequal forloop.counter 1 %}video_selected{% endifequal %}" data-src="{{ video.course_video_url }}">{{ video.course_video_type }}</div>
73
+                {% endfor %}
74
+            </div>
75
+        </div>
76
+
77
+        <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script>
78
+        <script src="//cdn.bootcss.com/video.js/6.2.8/video.min.js"></script>
79
+        <script>
80
+            $(function() {
81
+                var video_count = {{ video_count }};
82
+
83
+                $('.video_select_item').click(function () {
84
+                    $this = $(this);
85
+                    $('.video_select_item').removeClass('video_selected');
86
+                    $this.addClass('video_selected');
87
+                    $('#video').attr('src', $this.attr('data-src'));
88
+                })
89
+
90
+                $('#video')[0].onended = function() {
91
+                    var curscr = $(this).attr('data-cursrc');
92
+                    if (curscr >= video_count) {
93
+                        return
94
+                    }
95
+                    var next_video = $('#video' + (parseInt(curscr) + 1));
96
+                    $('#video').attr('src', next_video.attr('data-src'));
97
+                    $('.video_select_item').removeClass('video_selected');
98
+                    next_video.addClass('video_selected');
99
+                };
100
+            });
101
+        </script>
102
+        <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
103
+        <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
104
+        <script>
105
+            V.initWxData({
106
+                imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png',
107
+                link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk',
108
+                desc: '店员授权',
109
+                title: '店员授权',
110
+                timeLine: ''
111
+            }, true);
112
+            V.hideOptionMenu();
113
+
114
+            $('#scan').click(function () {
115
+                V.scanQRCode({
116
+                    needResult: 1
117
+                });
118
+            });
119
+            V.wxScanQRCodeSuccess = function (res) {
120
+                $('#code').val(V.parseScanQRCodeResultStr(res.resultStr));
121
+            }
122
+        </script>
123
+    </body>
124
+</html>

+ 77 - 0
page/templates/page/course_list.html

@@ -0,0 +1,77 @@
1
+{% load staticfiles %}
2
+
3
+<!DOCTYPE html>
4
+<html lang="zh-CN">
5
+    <head>
6
+        <meta charset="utf-8">
7
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
8
+        <meta name="format-detection" content="telephone=no,email=no,address=no">
9
+        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
10
+        <title>课程列表</title>
11
+
12
+        <link href="//res.wx.qq.com/open/libs/weui/0.4.3/weui.min.css" rel="stylesheet" type="text/css" />
13
+        <link href="{% static 'page/css/weui.ext.css' %}?v=1" rel="stylesheet" type="text/css" />
14
+
15
+        <style>
16
+            .container {
17
+                position: absolute;
18
+                top: 0;
19
+                right: 0;
20
+                bottom: 0;
21
+                left: 0;
22
+                background: #efefef;
23
+            }
24
+            .course_wrapper {
25
+                text-align: center;
26
+                background: white;
27
+                margin-bottom: 20px;
28
+                padding: 15px 0;
29
+            }
30
+            .course_name {
31
+                font-size: 18px;
32
+                font-weight: bold;
33
+                color: #020001;
34
+            }
35
+            .course_time {
36
+                font-size: 12px;
37
+                color: #999;
38
+            }
39
+            .course_cover>img {
40
+                width: 80%;
41
+                border-radius: 5px;
42
+            }
43
+        </style>
44
+    </head>
45
+    <body>
46
+        <div class="container" >
47
+            {% for course in courses %}
48
+                <div class="course_wrapper" data-courseid="{{ course.course_id }}">
49
+                    <div class="course_name">{{ course.course_name }}</div>
50
+                    <div class="course_time">{{ course.course_time }}分钟</div>
51
+                    <div class="course_cover"><img src="{{ course.course_cover_url }}"></div>
52
+                </div>
53
+            {% endfor %}
54
+        </div>
55
+
56
+        <script src="//cdn.bootcss.com/zepto/1.1.6/zepto.min.js"></script>
57
+        <script>
58
+            $(function() {
59
+                $('.course_wrapper').click(function () {
60
+                    window.location.href = '{{ domain }}/page/course/info?course_id=' + $(this).attr('data-courseid') + '&{{ params|safe }}';
61
+                })
62
+            });
63
+        </script>
64
+        <script type="text/javascript" src="//res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
65
+        <script type="text/javascript" src="{% static 'course/js/jswe-0.0.1.js' %}"></script>
66
+        <script>
67
+            V.initWxData({
68
+                imgUrl: 'http://pai.ai/static/pai2/img/paiai_96_96.png',
69
+                link: 'http://api.pai.ai/wx_oauth2?redirect_url=http://tamron.xfoto.com.cn/page/clerk',
70
+                desc: '店员授权',
71
+                title: '店员授权',
72
+                timeLine: ''
73
+            }, true);
74
+{#            V.hideOptionMenu();#}
75
+        </script>
76
+    </body>
77
+</html>

+ 4 - 0
page/tests.py

@@ -0,0 +1,4 @@
1
+from django.test import TestCase
2
+
3
+
4
+# Create your tests here.

+ 12 - 0
page/urls.py

@@ -0,0 +1,12 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf.urls import url
4
+
5
+from page import code_views, info_views, list_views
6
+
7
+
8
+urlpatterns = [
9
+    url(r'^course/code$', code_views.course_code, name='course_code'),
10
+    url(r'^course/list$', list_views.course_list, name='course_list'),
11
+    url(r'^course/info$', info_views.course_info, name='course_info'),
12
+]

+ 4 - 0
page/views.py

@@ -0,0 +1,4 @@
1
+from django.shortcuts import render
2
+
3
+
4
+# Create your views here.

+ 9 - 0
pep8.sh

@@ -0,0 +1,9 @@
1
+#!/bin/bash
2
+
3
+# Ignoring autogenerated files
4
+#  -- Migration directories
5
+# Ignoring error codes
6
+#  -- E128 continuation line under-indented for visual indent
7
+#  -- E501 line too long
8
+
9
+pep8 --exclude=migrations --ignore=E128,E501 .

+ 40 - 0
requirements.txt

@@ -0,0 +1,40 @@
1
+-e git+https://github.com/Brightcells/django-q.git#egg=django-q
2
+CodeConvert==2.0.4
3
+Django==1.11.3
4
+MySQL-python==1.2.5
5
+Pillow==3.4.2
6
+StatusCode==1.0.0
7
+TimeConvert==1.4.1
8
+cryptography==2.0.3
9
+django-curtail-uuid==1.0.0
10
+django-detect==1.0.5
11
+django-file-md5==1.0.1
12
+django-ip==1.0.1
13
+django-json-response==1.1.5
14
+django-logit==1.0.6
15
+django-multidomain==1.1.4
16
+django-paginator2==1.0.3
17
+django-rlog==1.0.7
18
+django-shortuuidfield==0.1.3
19
+django-six==1.0.2
20
+django-uniapi==1.0.0
21
+django-we==1.0.14
22
+djangorestframework==3.6.3
23
+furl==1.0.1
24
+hiredis==0.2.0
25
+isoweek==1.3.3
26
+jsonfield==2.0.2
27
+mock==2.0.0
28
+pep8==1.7.0
29
+pysnippets==1.0.4
30
+pywe-miniapp==1.0.0
31
+pywe-oauth==1.0.5
32
+pywe-response==1.0.1
33
+qiniu==7.1.5
34
+redis==2.10.6
35
+redis-extensions==1.1.1
36
+requests==2.18.4
37
+rlog==0.2
38
+shortuuid==0.5.0
39
+uWSGI==2.0.15
40
+versions==0.10.0

+ 0 - 0
utils/__init__.py


+ 0 - 0
utils/error/__init__.py


+ 51 - 0
utils/error/errno_utils.py

@@ -0,0 +1,51 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from StatusCode import BaseStatusCode, StatusCodeField
4
+
5
+
6
+class ProfileStatusCode(BaseStatusCode):
7
+    """ 用户相关错误码 4000xx """
8
+    PROFILE_NOT_FOUND = StatusCodeField(400001, 'Profile Not Found', description=u'用户不存在')
9
+
10
+
11
+class CourseCodeStatusCode(BaseStatusCode):
12
+    """ 课程兑换码相关错误码 4001xx """
13
+    COURSE_CODE_NOT_FOUND = StatusCodeField(400101, 'Course Code Not Found', description=u'课程兑换码不存在')
14
+    COURSE_CODE_HAS_EXCHANGED = StatusCodeField(400102, 'Course Code Has Exchanged', description=u'课程兑换码已兑换')
15
+
16
+
17
+class CourseStatusCode(BaseStatusCode):
18
+    """ 课程相关错误码 4002xx """
19
+    COURSE_NOT_FOUND = StatusCodeField(400201, 'Course Not Found', description=u'课程不存在')
20
+
21
+
22
+class OrderStatusCode(BaseStatusCode):
23
+    """ 订单/支付相关错误码 4040xx """
24
+    WX_UNIFIED_ORDER_FAIL = StatusCodeField(404000, 'WX Unified Order Fail', description=u'微信统一下单失败')
25
+    WX_ORDER_NOT_FOUND = StatusCodeField(404001, 'WX Order Not Found', description=u'订单不存在')
26
+    WX_ORDER_NOT_PAY = StatusCodeField(404002, 'WX Order Not Pay', description=u'订单未支付')
27
+    WX_ORDER_PAYING = StatusCodeField(404003, 'WX Order Paying', description=u'订单支付中')
28
+    WX_ORDER_PAY_FAIL = StatusCodeField(404009, 'WX Order Pay Fail', description=u'微信支付失败')
29
+    SIGN_CHECK_FAIL = StatusCodeField(404010, 'Sign Check Fail', description=u'签名校验失败')
30
+    FEE_CHECK_FAIL = StatusCodeField(404011, 'FEE Check Fail', description=u'金额校验失败')
31
+    NO_DETAIL_PERMISSION = StatusCodeField(404015, 'No Detail Permission', description=u'无详情权限')
32
+    WX_ORDER_PAID_ALREADY_EXISTS = StatusCodeField(404020, 'WX Order Paid Already Exists', description=u'照片已购买')
33
+
34
+
35
+class PayStatusCode(BaseStatusCode):
36
+    """ 支付相关错误码 4041xx """
37
+
38
+
39
+class WithdrawStatusCode(BaseStatusCode):
40
+    """ 提现相关错误码 4042xx """
41
+    BALANCE_NOT_ENOUGH = StatusCodeField(404200, 'Balance Not Enough', description=u'提现金额不足')
42
+
43
+
44
+class MessageStatusCode(BaseStatusCode):
45
+    """ 消息相关错误码 4090xx """
46
+    MESSAGE_NOT_FOUND = StatusCodeField(409001, 'Message Not Found', description=u'消息不存在')
47
+
48
+
49
+class TokenStatusCode(BaseStatusCode):
50
+    """ 票据相关错误码 4090xx """
51
+    TOKEN_NOT_FOUND = StatusCodeField(409901, 'Token Not Found', description=u'票据不存在')

+ 18 - 0
utils/error/response_utils.py

@@ -0,0 +1,18 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.http import JsonResponse
4
+from StatusCode import StatusCodeField
5
+
6
+
7
+def response_data(status_code=200, message=None, description=None, data={}, **kwargs):
8
+    return dict({
9
+        'status': status_code,
10
+        'message': message,
11
+        'description': description,
12
+        'data': data,
13
+    }, **kwargs)
14
+
15
+
16
+def response(status_code=200, message=None, description=None, data={}, **kwargs):
17
+    message, description = (message or status_code.message, description or status_code.description) if isinstance(status_code, StatusCodeField) else (message, description)
18
+    return JsonResponse(response_data(status_code, message, description, data, **kwargs), safe=False)

+ 0 - 0
utils/redis/__init__.py


+ 6 - 0
utils/redis/connect.py

@@ -0,0 +1,6 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf import settings
4
+
5
+
6
+r = settings.REDIS_CACHE

+ 68 - 0
utils/redis/rkeys.py

@@ -0,0 +1,68 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+# 唯一标识相关
4
+UUID_LIST = 'uuid:list'  # List,唯一标识列表
5
+
6
+# 用户相关
7
+PROFILE_INFO = 'profile:info:%s'  # STRING,用户信息,user_id
8
+
9
+# 导游相关
10
+TOUR_GUIDE_GROUP_GEO_INFO = 'tour:guide:group:geo:info:%s'  # ZSET,旅游团地理位置信息,group_id
11
+TOUR_GUIDE_GROUP_GEO_SUBMIT_DT = 'tour:guide:group:geo:submit:dt:%s'  # ZSET,旅游团地理位置最后上传时间,group_id
12
+TOUR_GUIDE_GROUP_CUR_SESSION = 'tour:guide:group:cur:session:%s'  # STRING,旅游团当前Session,group_id,导游设置集合时间的时候更新
13
+TOUR_GUIDE_GROUP_CUR_GATHER_INFO = 'tour:guide:group:cur:gather:info:%s'  # STRING,旅游团当前Session,group_id,导游设置集合时间的时候更新
14
+TOUR_GUIDE_GROUP_USER_GEO_LIST = 'tour:guide:group:user:geo:list:%s:%s:%s'  # LIST,旅游团当前用户地理位置列表,group_id、session_id、user_id
15
+
16
+TOUR_GUIDE_GROUP_USER_OWN = 'tour:guide:group:user:own:%s'  # STRING,导游当前拥有的旅行团,user_id,导游创建旅行团的时候更新
17
+TOUR_GUIDE_GROUP_USER_BELONG = 'tour:guide:group:user:belong:%s'  # STRING,用户当前所属旅行团,user_id,用户加入旅行团的时候更新
18
+
19
+# 群组相关
20
+GROUP_INFO = 'group:info:%s'  # STRING,群组信息,group_id
21
+
22
+# 群组用户相关
23
+GROUP_USERS_INFO = 'group:users:info:%s'  # STRING,群组用户信息,group_id
24
+GROUP_USERS_KV_INFO = 'group:users:kv:info:%s'  # STRING,群组用户信息,group_id
25
+GROUP_USERS_APPLYING_SET = 'group:users:applying:set:%s'  # SET,群组用户申请集合,group_id
26
+GROUP_USERS_PASSED_SET = 'group:users:passed:set:%s'  # SET,群组用户通过集合,group_id
27
+GROUP_USERS_REFUSED_SET = 'group:users:refused:set:%s'  # SET,群组用户拒绝集合,group_id
28
+GROUP_USERS_DELETED_SET = 'group:users:deleted:set:%s'  # SET,群组用户移除集合,group_id
29
+GROUP_USERS_QUIT_SET = 'group:users:quit:set:%s'  # SET,群组用户退出集合,group_id
30
+
31
+# 群组照片相关
32
+GROUP_PHOTO_DATA = 'group:photo:data:%s'  # STRING,群组数据记录,group_id
33
+GROUP_PHOTO_THUMB_UP = 'group:photo:thumb:up:%s:%s'  # STRING,群组照片用户点赞记录,photo_id、user_id
34
+GROUP_PHOTO_COMMENT_LIST = 'group:photo:comment:list:%s'  # STRING,群组照片用户评论列表,photo_id
35
+GROUP_PHOTO_THUMB_UP_LIST = 'group:photo:thumb:up:list:%s'  # STRING,群组照片用户点赞列表,photo_id
36
+GROUP_PHOTO_WATCHER_SET = 'group:photo:watcher:set:%s'  # SET,群组照片用户关注集合,photo_id,关注即评论点赞
37
+GROUP_LAST_PHOTO_PK = 'group:last:photo:pk:%s'  # STRING,群组最后一张照片PK,group_id
38
+
39
+# 摄影师照片相关
40
+LENSMAN_PHOTO_ORDER_RECORD = 'lensman:photo:order:record:%s:%s'  # STRING,摄影师照片购买记录,photo_id、user_id
41
+
42
+# 摄影师简报相关
43
+# 收入
44
+TOTAL_INCOME = 'total:income:%s:%s'  # STRING,总收入,user_id、photo_type
45
+WEEK_INCOME = 'week:income:%s:%s:%s'  # STRING,周收入,user_id、photo_type、Week.thisweek().isoformat()
46
+TODAY_INCOME = 'today:income:%s:%s:%s'  # STRING,日收入,user_id、photo_type、tc.local_string(format='%Y%m%d')
47
+# 上传
48
+TODAY_UPLOAD_PHOTO_AMOUNT = 'today:upload:photo:amount:%s:%s'  # STRING,日上传照片数量,user_id、tc.local_string(format='%Y%m%d')
49
+# 售出
50
+WEEK_SOLD = 'week:sold:%s:%s:%s'  # STRING,周售出,user_id、photo_type、Week.thisweek().isoformat()
51
+
52
+# 摄影师定价相关
53
+LENSMAN_PHOTO_PRICE_FIXED = 'lensman:photo:price:fixed:%s'  # STRING,摄影师照片定价(单位:分),user_id
54
+
55
+# 系统消息相关
56
+SYSTEM_MESSAGE_READ_INFO = 'system:message:read:info:%s'  # STRING,系统消息读取信息,user_id
57
+SYSTEM_MESSAGE_DELETED_INFO = 'system:message:deleted:info:%s'  # STRING,系统消息删除信息,user_id
58
+
59
+# 游客入口相关
60
+GUEST_ENTRANCE_CONTROL_INFO = 'guest:entrance:control:info:%s'  # STRING,游客入口控制信息,src
61
+
62
+# APP 相关
63
+LATEST_APP_INFO = 'latest:app:info:%s'  # STRING,最新 APP 信息,src
64
+APP_SETTINGS_INFO = 'app:settings:info:%s:%s:%s'  # STRING,APP 设置信息,platform、channel、version
65
+APP_PATCH_INFO = 'app:patch:info:%s:%s:%s'  # STRING,APP 补丁信息,platform、version、src
66
+
67
+# BOX 相关
68
+BOX_PROGRAM_VERSION_INFO = 'box:program:version:info'  # STRING,BOX 程序版本信息

+ 7 - 0
utils/url_utils.py

@@ -0,0 +1,7 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from django.conf import settings
4
+
5
+
6
+def upload_file_url(file_path):
7
+    return file_path and ('{}{}'.format(settings.DOMAIN, file_path.url)) or ''